/*
 * Decompiled with CFR 0.146.
 * 
 * Could not load the following classes:
 *  net.minecraft.util.ResourceLocation
 *  net.minecraftforge.client.model.obj.TextureCoordinate
 *  org.lwjgl.opengl.GL11
 *  org.lwjgl.util.vector.Matrix4f
 */
package mogiksaop.qliphoth.client.models.smd;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.util.ResourceLocation;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.ARBBufferObject;
import org.lwjgl.opengl.GL11;

import mogiksaop.qliphoth.common.Qliphoth;
import mogiksaop.qliphoth.common.util.RegexPatterns;
import mogiksaop.qliphoth.common.util.helpers.CommonHelper;
import mogiksaop.qliphoth.common.util.helpers.VectorHelper;

public class SmdModel {
	public final ValveStudioModel owner;
	public ArrayList<NormalizedFace> faces;
	public ArrayList<DeformVertex> verts;
	public ArrayList<Bone> bones;
	public HashMap<String, Bone> nameToBoneMapping;
	public HashMap<String, Material> materialsByName;
	public HashMap<Material, ArrayList<NormalizedFace>> facesByMaterial;
	public SmdAnimation currentAnim;
	private int vertexIDBank;
	protected boolean isBodyGroupPart;
	int lineCount;
	public Bone root;
	public int vertexVbo;
	public int textureVbo;
	public int normalsVbo;
	private FloatBuffer vertexBuffer;
	private FloatBuffer normalBuffer;

	SmdModel(SmdModel model, ValveStudioModel owner) {
		this.faces = new ArrayList(0);
		this.verts = new ArrayList(0);
		this.bones = new ArrayList(0);
		this.nameToBoneMapping = new HashMap();

		this.vertexIDBank = 0;

		this.lineCount = -1;

		this.vertexVbo = -1;
		this.textureVbo = -1;
		this.normalsVbo = -1;
		this.owner = owner;
		this.isBodyGroupPart = model.isBodyGroupPart;
		for (NormalizedFace face : model.faces) {
			DeformVertex[] vertices = new DeformVertex[face.vertices.length];
			for (int i = 0; i < vertices.length; i++) {
				DeformVertex d = new DeformVertex(face.vertices[i]);
				CommonHelper.ensureIndex(this.verts, d.ID);
				this.verts.set(d.ID, d);
			}
		}
		this.faces.addAll((Collection) model.faces.stream().map(face -> new NormalizedFace(face, this.verts))
				.collect(Collectors.toList()));
		for (int i = 0; i < model.bones.size(); i++) {
			Bone b = (Bone) model.bones.get(i);
			this.bones.add(new Bone(b, null, this));
		}
		for (int i = 0; i < model.bones.size(); i++) {
			Bone b = (Bone) model.bones.get(i);
			b.copy.setChildren(b, this.bones);
		}
		this.root = model.root.copy;
		owner.sendBoneData(this);
	}

	SmdModel(ValveStudioModel owner, ResourceLocation resloc) throws GabeNewellException {
		this.faces = new ArrayList(0);
		this.verts = new ArrayList(0);
		this.bones = new ArrayList(0);
		this.nameToBoneMapping = new HashMap();
		this.vertexIDBank = 0;
		this.lineCount = -1;
		this.vertexVbo = -1;
		this.textureVbo = -1;
		this.normalsVbo = -1;
		this.owner = owner;
		this.isBodyGroupPart = false;
		if (resloc.getPath().endsWith(".bmd")) {
			loadBmdModel(resloc, null);
		} else {
			loadSmdModel(resloc, null);
		}
		setBoneChildren();
		determineRoot();
		owner.sendBoneData(this);
		ValveStudioModel.print("Number of vertices = " + this.verts.size());
	}

	private void loadSmdModel(ResourceLocation resloc, SmdModel body) throws GabeNewellException {
		InputStream inputStream = Qliphoth.proxy.getStreamForResourceLocation(resloc);
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
			this.lineCount = 0;
			String currentLine;
			while ((currentLine = reader.readLine()) != null) {
				this.lineCount++;
				if (currentLine.startsWith("version"))
					continue;
				if (currentLine.startsWith("nodes")) {
					this.lineCount++;
					while (!(currentLine = reader.readLine()).startsWith("end")) {
						this.lineCount++;
						parseBone(currentLine, this.lineCount, body);
					}
					ValveStudioModel.print("Number of model bones = " + this.bones.size());
					continue;
				}
				if (currentLine.startsWith("skeleton")) {
					this.lineCount++;
					reader.readLine();
					this.lineCount++;
					while (!(currentLine = reader.readLine()).startsWith("end")) {
						this.lineCount++;
						if (!this.isBodyGroupPart)
							parseBoneValues(currentLine, this.lineCount);
					}
					continue;
				}
				if (currentLine.startsWith("triangles")) {
					this.lineCount++;
					while (!(currentLine = reader.readLine()).startsWith("end")) {
						Material mat = this.owner.usesMaterials ? requestMaterial(currentLine) : null;
						String[] params = new String[3];
						for (int i = 0; i < 3; i++) {
							this.lineCount++;
							params[i] = reader.readLine();
						}
						parseFace(params, this.lineCount, mat);
					}
				}
			}
		} catch (Exception e) {
			if (this.lineCount == -1)
				throw new GabeNewellException("there was a problem opening the model file : " + resloc, e);
			throw new GabeNewellException(
					"an error occurred reading the SMD file \"" + resloc + "\" on line #" + this.lineCount, e);
		}
		ValveStudioModel.print("Number of faces = " + this.faces.size());
	}

	private void loadBmdModel(ResourceLocation modelLoc, SmdModel body) throws GabeNewellException {
		InputStream inputStream = Qliphoth.proxy.getStreamForResourceLocation(modelLoc);
		try (DataInputStream in = new DataInputStream(inputStream)) {
			byte version = in.readByte();
			assert version == 1;
			int numNodes = in.readShort();
			CommonHelper.ensureIndex(this.bones, numNodes - 1);
			for (int i = 0; i < numNodes; i++) {
				short boneID = in.readShort();
				short parentBone = in.readShort();
				String name = readNullTerm(in);
				Bone parent = (parentBone != -1) ? (Bone) this.bones.get(parentBone) : null;
				this.bones.set(boneID, new Bone(name, boneID, parent, this));
			}
			int numSkeletons = in.readShort();
			short i;
			for (i = 0; i < numSkeletons; i = (short) (i + 1)) {
				int numBones = in.readShort();
				for (int j = 0; j < numBones; j++) {
					short boneId = in.readShort();
					float locX = in.readFloat();
					float locY = in.readFloat();
					float locZ = in.readFloat();
					float rotX = in.readFloat();
					float rotY = in.readFloat();
					float rotZ = in.readFloat();
					Bone theBone = (Bone) this.bones.get(boneId);
					theBone.setRest(VectorHelper.matrix4FromLocRot(locX, -locY, -locZ, rotX, -rotY, -rotZ));
				}
			}
			List<String> material = new ArrayList<String>();
			int numMaterial = in.readShort();
			for (int i1 = 0; i1 < numMaterial; i1++)
				material.add(readNullTerm(in));
			int vertexCount = 0;
			int numTriangles = in.readShort();
			for (int i1 = 0; i1 < numTriangles; i1++) {
				String mat = (String) material.get(in.readByte());
				DeformVertex[] faceVerts = new DeformVertex[3];
				TextureCoordinate[] uvs = new TextureCoordinate[3];
				for (int j = 0; j < 3; j++) {
					in.readShort();
					float x = in.readFloat();
					float y = -in.readFloat();
					float z = -in.readFloat();
					float normX = in.readFloat();
					float normY = -in.readFloat();
					float normZ = -in.readFloat();
					float u = in.readFloat();
					float v = in.readFloat();
					int id = vertexCount++;
					DeformVertex dv = getExisting(x, y, z);
					faceVerts[j] = (dv != null) ? dv : new DeformVertex(x, y, z, normX, normY, normZ, id);
					byte links = in.readByte();
					for (int w = 0; w < links; w++) {
						int boneID = in.readShort();
						float weight = in.readFloat();
						((Bone) this.bones.get(boneID)).addVertex(faceVerts[j], weight);
					}
					CommonHelper.ensureIndex(this.verts, id);
					this.verts.set(id, faceVerts[j]);
					uvs[j] = new TextureCoordinate(u, 1.0F - v);
				}
				NormalizedFace face = new NormalizedFace(faceVerts, uvs);
				face.vertices = faceVerts;
				face.textureCoordinates = uvs;
				this.faces.add(face);
				if (this.owner.usesMaterials) {
					Material mater = requestMaterial(mat);
					if (this.facesByMaterial == null)
						this.facesByMaterial = new HashMap();
					ArrayList<NormalizedFace> list = (ArrayList) this.facesByMaterial.computeIfAbsent(mater,
							k -> new ArrayList());
					list.add(face);
				}
			}
		} catch (IOException e) {
			throw new GabeNewellException("An error occurred while reading BMD " + modelLoc.getPath(), e);
		}
	}

	private Material requestMaterial(String materialName) throws GabeNewellException {
		if (!this.owner.usesMaterials)
			return null;
		if (this.materialsByName == null)
			this.materialsByName = new HashMap();
		Material result = (Material) this.materialsByName.get(materialName);
		if (result != null)
			return result;
		String materialPath = this.owner.getMaterialPath(materialName);
		URL materialURL = SmdModel.class.getResource(materialPath);
		try {
			File materialFile = new File(materialURL.toURI());
			result = new Material(materialFile);
			this.materialsByName.put(materialName, result);
			return result;
		} catch (Exception e) {
			throw new GabeNewellException(e);
		}
	}

	private void parseBone(String line, int lineCount, SmdModel body) {
		String[] params = line.split("\"");
		int id = Integer.parseInt(RegexPatterns.SPACE_SYMBOL.matcher(params[0]).replaceAll(""));
		String boneName = params[1];
		Bone theBone = (body != null) ? body.getBoneByName(boneName) : null;
		if (theBone == null) {
			int parentID = Integer.parseInt(RegexPatterns.SPACE_SYMBOL.matcher(params[2]).replaceAll(""));
			Bone parent = (parentID >= 0) ? (Bone) this.bones.get(parentID) : null;
			theBone = new Bone(boneName, id, parent, this);
		}
		CommonHelper.ensureIndex(this.bones, id);
		this.bones.set(id, theBone);
		this.nameToBoneMapping.put(boneName, theBone);
		ValveStudioModel.print(boneName);
	}

	SmdModel(ValveStudioModel owner, ResourceLocation resloc, SmdModel body) throws GabeNewellException {
		this.faces = new ArrayList(0);
		this.verts = new ArrayList(0);
		this.bones = new ArrayList(0);
		this.nameToBoneMapping = new HashMap();
		this.vertexIDBank = 0;
		this.lineCount = -1;
		this.vertexVbo = -1;
		this.textureVbo = -1;
		this.normalsVbo = -1;
		this.owner = owner;
		this.isBodyGroupPart = true;
		if (resloc.getPath().endsWith(".bmd")) {
			loadBmdModel(resloc, body);
		} else {
			loadSmdModel(resloc, body);
		}
		setBoneChildren();
		determineRoot();
		owner.sendBoneData(this);
	}

	private void parseBoneValues(String line, int lineCount) {
		String[] params = RegexPatterns.MULTIPLE_WHITESPACE.split(line);
		int id = Integer.parseInt(params[0]);
		float[] locRots = new float[6];
		for (int i = 1; i < 7; i++)
			locRots[i - 1] = Float.parseFloat(params[i]);
		Bone theBone = (Bone) this.bones.get(id);
		theBone.setRest(VectorHelper.matrix4FromLocRot(locRots[0], -locRots[1], -locRots[2], locRots[3], -locRots[4],
				-locRots[5]));
	}

	private void parseFace(String[] params, int lineCount, Material mat) {
		DeformVertex[] faceVerts = new DeformVertex[3];
		TextureCoordinate[] uvs = new TextureCoordinate[3];
		for (int i = 0; i < 3; i++) {
			String[] values = RegexPatterns.MULTIPLE_WHITESPACE.split(params[i]);
			float x = Float.parseFloat(values[1]), y = -Float.parseFloat(values[2]), z = -Float.parseFloat(values[3]),
					xn = Float.parseFloat(values[4]), yn = -Float.parseFloat(values[5]);
			float zn = -Float.parseFloat(values[6]);
			DeformVertex v = getExisting(x, y, z);
			if (v == null) {
				faceVerts[i] = new DeformVertex(x, y, z, xn, yn, zn, this.vertexIDBank);
				CommonHelper.ensureIndex(this.verts, this.vertexIDBank);
				this.verts.set(this.vertexIDBank, faceVerts[i]);
				this.vertexIDBank++;
			} else {
				faceVerts[i] = v;
			}
			uvs[i] = new TextureCoordinate(Float.parseFloat(values[7]), 1.0F - Float.parseFloat(values[8]));
			if (values.length > 10)
				doBoneWeights(values, faceVerts[i]);
		}
		NormalizedFace face = new NormalizedFace(faceVerts, uvs);
		face.vertices = faceVerts;
		face.textureCoordinates = uvs;
		this.faces.add(face);
		if (mat != null) {
			if (this.facesByMaterial == null)
				this.facesByMaterial = new HashMap();
			ArrayList<NormalizedFace> list = (ArrayList) this.facesByMaterial.get(mat);
			if (list == null)
				this.facesByMaterial.put(mat, list = new ArrayList<NormalizedFace>());
			list.add(face);
		}
	}

	private DeformVertex getExisting(float x, float y, float z) {
		for (DeformVertex v : this.verts) {
			if (v.equals(x, y, z))
				return v;
		}
		return null;
	}

	public void render(boolean hasChanged) {
//		boolean isPokeball = false;
//		if (this.owner.resource.getPath().contains("pokeballs"))
//			isPokeball = true;
//		boolean smooth = isPokeball ? PixelmonConfig.enableSmoothPokeballShading
//				: PixelmonConfig.enableSmoothPokemonShading;
		boolean smooth=true;
		if (this.owner.overrideSmoothShading) {
			smooth = false;
		}
		boolean useVBO = OpenGlHelper.useVbo();

		if (!useVBO) {
			GL11.glBegin(4);
			if (!this.owner.usesMaterials) {
				for (NormalizedFace f : this.faces) {
					f.addFaceForRender(smooth);
				}
			} else {
				for (Map.Entry<Material, ArrayList<NormalizedFace>> entry : this.facesByMaterial.entrySet()) {
					if (entry.getKey() == null)
						continue;
					((Material) entry.getKey()).pre();
					for (NormalizedFace face : entry.getValue()) {
						face.addFaceForRender(smooth);
					}
					((Material) entry.getKey()).post();
				}
			}
			GL11.glEnd();
		} else {
			if (hasChanged) {
				if (this.vertexVbo == -1) {
					this.vertexVbo = ARBBufferObject.glGenBuffersARB();
					this.textureVbo = ARBBufferObject.glGenBuffersARB();
					this.normalsVbo = ARBBufferObject.glGenBuffersARB();
					buildVBO(smooth);
				} else {
					updateVBO(smooth);
				}
			}

			OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.vertexVbo);
			GL11.glVertexPointer(3, 5126, 0, 0L);

			OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.textureVbo);
			GL11.glTexCoordPointer(2, 5126, 0, 0L);

			OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.normalsVbo);
			GL11.glNormalPointer(5126, 0, 0L);

			GL11.glEnableClientState(32884);
			GL11.glEnableClientState(32888);
			GL11.glEnableClientState(32885);
			GL11.glDrawArrays(4, 0, this.faces.size() * 3);
			GL11.glDisableClientState(32884);
			GL11.glDisableClientState(32888);
			GL11.glDisableClientState(32885);

			OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, 0);
		}
	}

	private void doBoneWeights(String[] values, DeformVertex vert) {
		int links = Integer.parseInt(values[9]);
		float[] weights = new float[links];
		float sum = 0.0F;
		for (int i = 0; i < links; i++) {
			weights[i] = Float.parseFloat(values[i * 2 + 11]);
			sum += weights[i];
		}
		for (int i = 0; i < links; i++) {
			int boneID = Integer.parseInt(values[i * 2 + 10]);
			float weight = weights[i] / sum;
			((Bone) this.bones.get(boneID)).addVertex(vert, weight);
		}
	}

	private void setBoneChildren() {
		for (int i = 0; i < this.bones.size(); i++) {
			Bone theBone = (Bone) this.bones.get(i);
			this.bones.stream().filter(child -> (child.parent == theBone)).forEach(theBone::addChild);
		}
	}

	private void determineRoot() {
		for (Bone b : this.bones) {
			if (b.parent == null && !b.children.isEmpty()) {
				this.root = b;
				break;
			}
		}
		if (this.root == null)
			for (Bone b : this.bones) {
				if (!b.name.equals("blender_implicit")) {
					this.root = b;
					break;
				}
			}
	}

	public void setAnimation(SmdAnimation anim) {
		this.currentAnim = anim;
	}

	public Bone getBoneByID(int id) {
		try {
			return (Bone) this.bones.get(id);
		} catch (IndexOutOfBoundsException e) {
			return null;
		}
	}

	public Bone getBoneByName(String name) {
		for (Bone b : this.bones) {
			if (b.name.equals(name))
				return b;
		}
		return null;
	}

	public AnimFrame currentFrame() {
		return (this.currentAnim == null) ? null
				: ((this.currentAnim.frames == null) ? null
						: (this.currentAnim.frames.isEmpty() ? null
								: (AnimFrame) this.currentAnim.frames.get(this.currentAnim.currentFrameIndex)));
	}

	public void resetVerts() {
		this.verts.forEach(DeformVertex::reset);
	}

	private void updateVBO(boolean smoothShading) {
		this.vertexBuffer.clear();
		this.normalBuffer.clear();
		for (NormalizedFace face : this.faces) {
			face.addFaceForRender(this.vertexBuffer, this.normalBuffer, smoothShading);
		}

		this.vertexBuffer.flip();
		OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.vertexVbo);
		ARBBufferObject.glBufferDataARB(OpenGlHelper.GL_ARRAY_BUFFER, this.vertexBuffer, 35044);

		this.normalBuffer.flip();
		OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.normalsVbo);
		ARBBufferObject.glBufferDataARB(OpenGlHelper.GL_ARRAY_BUFFER, this.normalBuffer, 35044);
	}

	private void buildVBO(boolean smoothShading) {
		this.vertexBuffer = BufferUtils.createFloatBuffer(this.faces.size() * 9);
		FloatBuffer textureBuffer = BufferUtils.createFloatBuffer(this.faces.size() * 6);
		this.normalBuffer = BufferUtils.createFloatBuffer(this.faces.size() * 9);

		for (NormalizedFace face : this.faces) {
			face.addFaceForRender(this.vertexBuffer, textureBuffer, this.normalBuffer, smoothShading);
		}

		this.vertexBuffer.flip();
		OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.vertexVbo);
		ARBBufferObject.glBufferDataARB(OpenGlHelper.GL_ARRAY_BUFFER, this.vertexBuffer, 35044);

		textureBuffer.flip();
		OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.textureVbo);
		ARBBufferObject.glBufferDataARB(OpenGlHelper.GL_ARRAY_BUFFER, textureBuffer, 35044);

		this.normalBuffer.flip();
		OpenGlHelper.glBindBuffer(OpenGlHelper.GL_ARRAY_BUFFER, this.normalsVbo);
		ARBBufferObject.glBufferDataARB(OpenGlHelper.GL_ARRAY_BUFFER, this.normalBuffer, 35044);
	}

	private static String readNullTerm(DataInputStream in) throws IOException {
		StringBuilder str = new StringBuilder();
		char ch = Character.MIN_VALUE;
		do {
			if (ch != '\000')
				str.append(ch);
			ch = in.readChar();
		} while (ch != '\000');
		return str.toString();
	}
}
